Kealdish's Studio.

探究 iOS 11 下的 UIDebuggingInformationOverlay

字数统计: 1.2k阅读时长: 5 min
2017/11/18 Share

前言

自 iOS 9 以后,Apple 加入了悬浮窗调试工具,也就是 UIDebuggingInformationOverlay 。利用它我们可以做到很多事情,例如:查看视图层级,控制器层级,页面中的变量,测量等等。那么我们如何开启这个调试工具呢?只需要添加如下的代码:

1
2
3
4
let overlayClass = NSClassFromString("UIDebuggingInformationOverlay") as? UIWindow.Type
_ = overlayClass?.perform(NSSelectorFromString("prepareDebuggingOverlay"))
let overlay = overlayClass?.perform(NSSelectorFromString("overlay")).takeUnretainedValue() as? UIWindow
_ = overlay?.perform(NSSelectorFromString("toggleVisibility"))

这段代码的实际意义可以转换成以下两行代码:

1
2
[UIDebuggingInformationOverlay prepareDebuggingOverlay];
[[UIDebuggingInformationOverlay overlay] toggleVisibility];

调用成功后,我们就能看到悬浮窗的庐山真面目了。

然而,在 iOS 11 后,上面的代码就不再 work 了。获取信息后得知是 Apple 添加了验证机制,想以此来确保只有内部的 App 链接到 UIKit 后才可以访问这些私有类。在 iOS 11之后,我们还有办法去使用这种调试工具吗?

内部实现探究

利用 LLDB 尝试去逆向 -[UIDebuggingInformationOverlay init] ,发现在 iOS 10 下,该方法的实现大致如下:

1
2
3
4
5
6
7
8
9
10
@implementation UIDebuggingInformationOverlay
- (instancetype)init {
if (self = [super init]) {
[self _setWindowControlsStatusBarOrientation:NO];
}
return self;
}
@end

使用同样的方法去逆向 iOS 11 下的 -[UIDebuggingInformationOverlay init] ,该方法的实现大致是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@implementation UIDebuggingInformationOverlay
- (instancetype)init {
static BOOL overlayEnabled = NO;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
overlayEnabled = UIDebuggingOverlayIsEnabled();
});
if (!overlayEnabled) {
return nil;
}
if (self = [super init]) {
[self _setWindowControlsStatusBarOrientation:NO];
}
return self;
}
@end

从如上代码中我们可以看出,Apple 使用 UIDebuggingOverlayIsEnabled() 去验证当前设备是否是内部设备,所以,当我们去调用 [UIDebuggingInformationOverlay new] 时,会返回 nil

如何绕过验证机制

Serek Selander 在 Swizzling in iOS 11 with UIDebuggingInformationOverlay 文章中给出了他的方案。他的原理是通过 LLDB 找出 dispatch_once 部分代码在内存地址中的范围。dispatch_once 内部实现上是这样一个流程onceToken 的地址与 -1 进行比较,如果包含 -1 ,就表示已经执行过 block 中的代码,不再执行,若不包含 -1 , 则会去执行 block 中的代码,并将 onceToken 的地址置为 -1(默认初始化为 0)。他的做法就是找到 mainHandler.onceToken 的内存地址,然后将 -1 写入到该内存地址中。完整的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
@import UIKit;
@import Foundation;
@import ObjectiveC.runtime; // you mean ObjectiveC.funtime, ooooooooooooooHHHHHH
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wincomplete-implementation"
#pragma clang diagnostic ignored "-Wundeclared-selector"
//*****************************************************************************/
#pragma mark - Section 0 - Private Declarations
//*****************************************************************************/
@interface NSObject()
- (void)_setWindowControlsStatusBarOrientation:(BOOL)orientation;
@end
//*****************************************************************************/
#pragma mark - Section 1 - FakeWindowClass
//*****************************************************************************/
@interface FakeWindowClass : UIWindow
@end
@implementation FakeWindowClass
- (instancetype)initSwizzled
{
if (self= [super init]) {
[self _setWindowControlsStatusBarOrientation:NO];
}
return self;
}
@end
//*****************************************************************************/
#pragma mark - Section 2 - Initialization
//*****************************************************************************/
@implementation NSObject (UIDebuggingInformationOverlayInjector)
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class cls = NSClassFromString(@"UIDebuggingInformationOverlay");
NSAssert(cls, @"DBG Class is nil?");
// Swizzle code here
[FakeWindowClass swizzleOriginalSelector:@selector(init) withSizzledSelector:@selector(initSwizzled) forClass:cls isClassMethod:NO];
[self swizzleOriginalSelector:@selector(prepareDebuggingOverlay) withSizzledSelector:@selector(prepareDebuggingOverlaySwizzled) forClass:cls isClassMethod:YES];
});
}
+ (void)swizzleOriginalSelector:(SEL)originalSelector withSizzledSelector:(SEL)swizzledSelector forClass:(Class)class isClassMethod:(BOOL)isClassMethod
{
Method originalMethod;
Method swizzledMethod;
if (isClassMethod) {
originalMethod = class_getClassMethod(class, originalSelector);
swizzledMethod = class_getClassMethod([self class], swizzledSelector);
} else {
originalMethod = class_getInstanceMethod(class, originalSelector);
swizzledMethod = class_getInstanceMethod([self class], swizzledSelector);
}
NSAssert(originalMethod, @"originalMethod should not be nil");
NSAssert(swizzledMethod, @"swizzledMethod should not be nil");
method_exchangeImplementations(originalMethod, swizzledMethod);
}
//*****************************************************************************/
#pragma mark - Section 3 - prepareDebuggingOverlay
//*****************************************************************************/
+ (void)prepareDebuggingOverlaySwizzled {
Class cls = NSClassFromString(@"UIDebuggingInformationOverlay");
SEL sel = @selector(prepareDebuggingOverlaySwizzled);
Method m = class_getClassMethod(cls, sel);
IMP imp = method_getImplementation(m);
void (*methodOffset) = (void *)((imp + (long)27));
void *returnAddr = &&RETURNADDRESS;
__asm__ __volatile__(
"pushq %0\n\t"
"pushq %%rbp\n\t"
"movq %%rsp, %%rbp\n\t"
"pushq %%r15\n\t"
"pushq %%r14\n\t"
"pushq %%r13\n\t"
"pushq %%r12\n\t"
"pushq %%rbx\n\t"
"pushq %%rax\n\t"
"jmp *%1\n\t"
:
: "r" (returnAddr), "r" (methodOffset));
RETURNADDRESS: ;
}
@end
#pragma clang diagnostic pop

改进

由于模拟器和真机的架构不同, Serek Selander 给出的代码只能在模拟器下 work ,因而为了能在真机下 work , 我做出了一些改进。完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// DebuggingOverlay.m
@import UIKit;
@import Foundation;
@import ObjectiveC.runtime;
// Used for swizzling on iOS 11+. UIDebuggingInformationOverlay is a subclass of UIWindow
@implementation UIWindow (DocsUIDebuggingInformationOverlaySwizzler)
- (instancetype)swizzle_basicInit {
return [super init];
}
// [[UIDebuggingInformationOverlayInvokeGestureHandler mainHandler] _handleActivationGesture:(UIGestureRecognizer *)]
// requires a UIGestureRecognizer, as it checks the state of it. We just fake that here.
- (UIGestureRecognizerState)state {
return UIGestureRecognizerStateEnded;
}
@end
@interface DebuggingOverlay : NSObject
@end
@implementation DebuggingOverlay
+ (void)toggleOverlay {
// In iOS 11, Apple added additional checks to disable this overlay unless the
// device is an internal device. To get around this, we swizzle out the
// -[UIDebuggingInformationOverlay init] method (which returns nil now if
// the device is non-internal), and we call:
// [[UIDebuggingInformationOverlayInvokeGestureHandler mainHandler] _handleActivationGesture:(UIGestureRecognizer *)]
// to show the window, since that now adds the debugging view controllers, and calls
// [overlay toggleVisibility] for us.
if (@available(iOS 11.0, *)) {
id debugInfoClass = NSClassFromString(@"UIDebuggingInformationOverlay");
id handlerClass = NSClassFromString(@"UIDebuggingInformationOverlayInvokeGestureHandler");
UIWindow *window = [[UIWindow alloc] init];
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// Swizzle init of debugInfo class
Method originalInit = class_getInstanceMethod(debugInfoClass, @selector(init));
IMP swizzledInit = [window methodForSelector:@selector(swizzle_basicInit)];
method_setImplementation(originalInit, swizzledInit);
});
id debugOverlayInstance = [debugInfoClass performSelector:NSSelectorFromString(@"overlay")];
[debugOverlayInstance setFrame:[[UIScreen mainScreen] bounds]];
id handler = [handlerClass performSelector:NSSelectorFromString(@"mainHandler")];
[handler performSelector:NSSelectorFromString(@"_handleActivationGesture:") withObject:window];
}
}
@end

主要思想是 UIDebuggingInformationOverlayUIWindow 的子类,那么我们可以利用 runtime 机制动态去替换其 init 方法的 IMP,以此来绕过 Apple 的验证机制。在实例化一个 UIDebuggingInformationOverlay 对象后,调用 [[UIDebuggingInformationOverlayInvokeGestureHandler mainHandler] _handleActivationGesture:(UIGestureRecognizer *)] 触发 UIDebuggingInformationOverlay 显示到屏幕上。由于该方法要求必须要有 UIGestureRecognizer 手势,以便检查其 state 值,因而我们需要添加 state 变量用来伪装手势。最后,在实际调用的地方调用如下代码即可 work :

1
2
id cls = NSClassFromString(@"DebuggingOverlay");
[cls performSelector:NSSelectorFromString(@"toggleOverlay")]
CATALOG
  1. 1. 前言
  2. 2. 内部实现探究
  3. 3. 如何绕过验证机制
  4. 4. 改进